#!/usr/bin/bash
#
# Copyright 2009-2018 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#

USAGE_TEXT="Usage: crm_failcount <command> [<options>]
Common options:
 --help                 Display this text, then exit
 --version              Display version information, then exit
 -V, --verbose          Specify multiple times to increase debug output
 -q, --quiet            Print only the value (if querying)

Commands:
 -G, --query            Query the current value of the resource's fail count
 -D, --delete           Delete resource's recorded failures

Additional Options:
 -r, --resource=value   Name of the resource to use (required)
 -n, --operation=value  Name of operation to use (instead of all operations)
 -I, --interval=value   If operation is specified, its interval
 -N, --node=value       Use failcount on named node (instead of local node)"


HELP_TEXT="crm_failcount - Query or delete resource fail counts

$USAGE_TEXT"

# These constants must track crm_exit_t values
CRM_EX_OK=0
CRM_EX_USAGE=64
CRM_EX_NOSUCH=105

exit_usage() {
	if [ $# -gt 0 ]; then
		echo "error:" "$@" >&2
	fi
	echo
	echo "$USAGE_TEXT"
	exit $CRM_EX_USAGE
}

warn() {
	echo "warning:" "$@" >&2
}

interval_re() {
	echo "^[[:blank:]]*([0-9]+)[[:blank:]]*(${1})[[:blank:]]*$"
}

# This function should follow crm_get_interval() as closely as possible
parse_interval() {
	INT_S="$1"

	INT_8601RE="^P(([0-9]+)Y)?(([0-9]+)M)?(([0-9]+)D)?T?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?$"
	
	if [[ $INT_S =~ $(interval_re "") ]]; then
		echo $(( ${BASH_REMATCH[1]} * 1000 ))

	elif [[ $INT_S =~ $(interval_re "s|sec") ]]; then
		echo $(( ${BASH_REMATCH[1]} * 1000 ))

	elif [[ $INT_S =~ $(interval_re "ms|msec") ]]; then
		echo "${BASH_REMATCH[1]}"

	elif [[ $INT_S =~ $(interval_re "m|min") ]]; then
		echo $(( ${BASH_REMATCH[1]} * 60000 ))

	elif [[ $INT_S =~ $(interval_re "h|hr") ]]; then
		echo $(( ${BASH_REMATCH[1]} * 3600000 ))

	elif [[ $INT_S =~ $(interval_re "us|usec") ]]; then
		echo $(( ${BASH_REMATCH[1]} / 1000 ))

	elif [[ $INT_S =~ ^P([0-9]+)W$ ]]; then
		echo $(( ${BASH_REMATCH[1]} * 604800000 ))

	elif [[ $INT_S =~ $INT_8601RE ]]; then
		echo $(( ( ${BASH_REMATCH[2]:-0} * 31536000000 ) \
			+ ( ${BASH_REMATCH[4]:-0} * 2592000000 ) \
			+ ( ${BASH_REMATCH[6]:-0} * 86400000 ) \
			+ ( ${BASH_REMATCH[8]:-0} * 3600000 ) \
			+ ( ${BASH_REMATCH[10]:-0} * 60000 ) \
			+ ( ${BASH_REMATCH[12]:-0} * 1000 ) ))

	else
		warn "Unrecognized interval, using 0"
		echo "0"
	fi
}

query_single_attr() {
	QSR_TARGET="$1"
	QSR_ATTR="$2"

	crm_attribute $VERBOSE --quiet --query -t status -d 0 \
		-N "$QSR_TARGET" -n "$QSR_ATTR"
}

query_attr_sum() {
	QAS_TARGET="$1"
	QAS_PREFIX="$2"

	# Build xpath to match all transient node attributes with prefix
	QAS_XPATH="/cib/status/node_state[@uname='${QAS_TARGET}']"
	QAS_XPATH="${QAS_XPATH}/transient_attributes/instance_attributes"
	QAS_XPATH="${QAS_XPATH}/nvpair[starts-with(@name,'$QAS_PREFIX')]"

	# Query attributes that match xpath
	# @TODO We ignore stderr because we don't want "no results" to look
	# like an error, but that also makes $VERBOSE pointless.
	QAS_ALL=$(cibadmin --query --sync-call --local \
		--xpath="$QAS_XPATH" 2>/dev/null)
	QAS_EX=$?

	# "No results" is not an error
	if [ $QAS_EX -ne $CRM_EX_OK ] && [ $QAS_EX -ne $CRM_EX_NOSUCH ]; then
		echo "error: could not query CIB for fail counts" >&2
		exit $QAS_EX
	fi

	# Extract the attribute values (one per line) from the output
	QAS_VALUE=$(echo "$QAS_ALL" | sed -n -e \
		's/.*<nvpair.*value="\([0-9][0-9]*\|INFINITY\)".*>.*/\1/p')

	# Sum the values
	QAS_SUM=0
	for i in 0 $QAS_VALUE; do
		if [ "$i" = "INFINITY" ]; then
			QAS_SUM="INFINITY"
			break
		else
			QAS_SUM=$(($QAS_SUM + $i))
		fi
	done
	if [ "$QAS_SUM" = "INFINITY" ]; then
		echo $QAS_SUM
	elif [ "$QAS_SUM" -ge 1000000 ]; then
		echo "INFINITY"
	else
		echo $QAS_SUM
	fi
}

query_failcount() {
	QF_TARGET="$1"
	QF_RESOURCE="$2"
	QF_OPERATION="$3"
	QF_INTERVAL="$4"

	QF_ATTR_RSC="fail-count-${QF_RESOURCE}"

	if [ -n "$QF_OPERATION" ]; then
		QF_ATTR_DISPLAY="${QF_ATTR_RSC}#${QF_OPERATION}_${QF_INTERVAL}"
		QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_DISPLAY")
	else
		QF_ATTR_DISPLAY="$QF_ATTR_RSC"
		QF_COUNT=$(query_attr_sum "$QF_TARGET" "${QF_ATTR_RSC}#")
	fi

	# @COMPAT attributes set < 1.1.17:
	# If we didn't find any per-operation failcount,
	# check whether there is a legacy per-resource failcount.
	if [ "$QF_COUNT" = "0" ]; then
		QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_RSC")
		if [ "$QF_COUNT" != "0" ]; then
			QF_ATTR_DISPLAY="$QF_ATTR_RSC"
		fi
	fi

	# Echo result (comparable to crm_attribute, for backward compatibility)
	if [ -n "$QUIET" ]; then
		echo $QF_COUNT
	else
		echo "scope=status  name=$QF_ATTR_DISPLAY value=$QF_COUNT"
	fi
}

clear_failcount() {
	CF_TARGET="$1"
	CF_RESOURCE="$2"
	CF_OPERATION="$3"
	CF_INTERVAL="$4"

	if [ -n "$CF_OPERATION" ]; then
		CF_OPERATION="-n $CF_OPERATION -I ${CF_INTERVAL}ms"
	fi
	crm_resource $QUIET $VERBOSE --cleanup \
		-N "$CF_TARGET" -r "$CF_RESOURCE" $CF_OPERATION
}

QUIET=""
VERBOSE=""

command=""
resource=""
operation=""
interval="0"
target=$(crm_node -n 2>/dev/null)

SHORTOPTS="qDGQVN:U:v:i:l:r:n:I:"

LONGOPTS_COMMON="help,version,verbose,quiet"
LONGOPTS_COMMANDS="query,delete"
LONGOPTS_OTHER="resource:,node:,operation:,interval:"
LONGOPTS_COMPAT="delete-attr,get-value,resource-id:,uname:,lifetime:,attr-value:,attr-id:"

LONGOPTS="$LONGOPTS_COMMON,$LONGOPTS_COMMANDS,$LONGOPTS_OTHER,$LONGOPTS_COMPAT"

TEMP=$(/usr/bin/getopt -o $SHORTOPTS --long $LONGOPTS -n crm_failcount -- "$@")
if [ $? -ne 0 ]; then
	exit_usage
fi
eval set -- "$TEMP" # Quotes around $TEMP are essential

while true ; do
	case "$1" in
		--help)
			echo "$HELP_TEXT"
			exit $CRM_EX_OK
			;;
		--version)
			crm_attribute --version
			exit $?
			;;
		-q|-Q|--quiet)
			QUIET="--quiet"
			shift
			;;
		-V|--verbose)
			VERBOSE="$VERBOSE $1"
			shift
			;;
		-G|--query|--get-value)
			command="--query"
			shift
			;;
		-D|--delete|--delete-attr)
			command="--delete"
			shift
			;;
		-r|--resource|--resource-id)
			resource="$2"
			shift 2
			;;
		-n|--operation)
			operation="$2"
			shift 2
			;;
		-I|--interval)
			interval="$2"
			shift 2
			;;
		-N|--node|-U|--uname)
			target="$2"
			shift 2
			;;
		-v|--attr-value)
			if [ "$2" = "0" ]; then
				command="--delete"
			else
				warn "ignoring deprecated option '$1' with nonzero value"
			fi
			shift 2
			;;
		-i|--attr-id|-l|--lifetime)
			warn "ignoring deprecated option '$1'"
			shift 2
			;;
		--)
			shift
			break
			;;
		*)
			exit_usage "unknown option '$1'"
			;;
	esac
done

[ -n "$command" ]  || exit_usage "must specify a command"
[ -n "$resource" ] || exit_usage "resource name required"
[ -n "$target" ]   || exit_usage "node name required"

interval=$(parse_interval $interval)

if [ "$command" = "--query" ]; then
	query_failcount "$target" "$resource" "$operation" "$interval"
else
	clear_failcount "$target" "$resource" "$operation" "$interval"
fi
